package security

import (
	"time"

	"gitee.com/hp-huiw/my-go-core/date"
	"github.com/golang-jwt/jwt/v5"
)

// More details check on https://godoc.org/github.com/golang-jwt/jwt/v5.

// It only holds user basic identification data which is embedded into others.
type UserBasicIdentity struct {
	UserID   string `json:"id"`
	Username string `json:"username"`
}

// It only holds user identification data which is embedded into others.
type UserIdentity struct {
	UserBasicIdentity
	AccountGroupID string  `json:"accountGroupId"`
	DeviceID       *string `json:"deviceId"`
}

// Golang does not support the traditional OOP inheritance either.
// Even you can create a base structure and embed it into a child structure. You still can not pass child structure to a function which expects base structure.
// If you only need method you definitely can use interface. But how about common fields?
// interface does not support field in Golang.
// The solution I come up with is method for field. It likes get property method in Java.
// Now you can pass BaseClaims and MyClaims to function which accepts StandardClaimsProvider interface.
type StandardClaimsProvider interface {
	GetStandardClaims() *jwt.RegisteredClaims
	GetExpirationTime() (*jwt.NumericDate, error)
	GetIssuedAt() (*jwt.NumericDate, error)
	GetNotBefore() (*jwt.NumericDate, error)
	GetIssuer() (string, error)
	GetSubject() (string, error)
	GetAudience() (jwt.ClaimStrings, error)
}

// This is the basic token claims which is encoded into refresh token.
type BaseClaims struct {
	jwt.RegisteredClaims
	UserIdentity
}

// Sets value only works on pointer type. We need to update on jwt.RegisteredClaims.
func (b *BaseClaims) GetStandardClaims() *jwt.RegisteredClaims {
	return &b.RegisteredClaims
}

// func (BaseClaims) Valid() error {
// 	return nil
// }

// This contains more data likes Roles. It is encoded into access token.
type MyClaims struct {
	BaseClaims
	Roles []string `json:"roles"`
}

// This is anything client needs.
// The odd tag name is for the client which works with spring security rest.
type MyUserDetails struct {
	UserIdentity
	FullName              string     `json:"fullName"`
	AvatarUrl             string     `json:"avatarUrl"`
	IsAccountGroupCreator bool       `json:"isAccountGroupCreator"`
	Roles                 []string   `json:"roles"`
	FullRoles             []FullRole `json:"fullRoles"`
	Bearer                string     `json:"bearer"`
	AccessToken           string     `json:"accessToken"`
	GeneratedAt           string     `json:"generatedAt"`
	ExpiredAt             string     `json:"expiredAt"`
	RefreshToken          string     `json:"refreshToken"`
	ExpiresIn             int64      `json:"expiresIn"`
}

type FullRole struct {
	ID        string `json:"id"`
	Authority string `json:"authority"`
	Title     string `json:"title"`
}

// Generates token and returns token string and error.
// It accepts ClaimContainer interface. That means you can pass in either BaseClaims or MyClaims.
// By using interfaces is the way that how golang works.
func GenerateToken(claims StandardClaimsProvider, signingKey string, expiresAt int64) (tokenString string, err error) {
	// Gets current date time for token issuedAt field.
	currentTime := date.GetCurrentDate()

	// Sets when token issued.
	rc := claims.GetStandardClaims()
	rc.IssuedAt = jwt.NewNumericDate(currentTime)
	rc.NotBefore = jwt.NewNumericDate(currentTime)

	// Sets the expiresAt if needed.
	if expiresAt > 0 {
		rc.ExpiresAt = jwt.NewNumericDate(currentTime.Add(time.Second * time.Duration(expiresAt)))
	}

	// Creates token with provides claims.
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// Gets jws with signing key.
	return token.SignedString([]byte(signingKey))
}

// Checks on access/refresh token to see if it is a valid one and does not expire yet.
// Refresh token does not check on expiration.
func ValidateToken(jws string, signingKey string) (claims *MyClaims, valid bool) {
	// Parses token string to claims and token structure.
	// claims, token, err := ParseToken(jws, signingKey)

	claims = &MyClaims{}

	// Parses input token string and updates claims.
	token, err := jwt.ParseWithClaims(jws, claims, func(token *jwt.Token) (any, error) {
		return []byte(signingKey), nil
	})

	if err != nil || !token.Valid {
		return claims, false
	}

	return claims, true
}
